/* Copyright 2011 Rok Lenarčič 
 * 
 * This file is a part of MicroFn.
 * 
 * MicroFn is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * MicroFn is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/> */

package net.rlenar.microfn;

import java.io.IOException;
import java.nio.CharBuffer;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.rlenar.microfn.RegExp.ReaderCharSequence.EndOfReaderException;
import net.rlenar.microfn.desc.FnName;


/**
 * Regular expression which also acts as a function:
 * 1 arg: findall
 * 2 arg: replace simple / replace multiple groups
 * 3 arg: replace specific group
 * 
 * @author rok.lenarcic
 *
 */
@FnName("Regular Expression")
public class RegExp extends Fn {
	private final Pattern pattern;
	/**
	 * Creates a new regular expression object with given pattern.
	 * The object is also a Fn and it invokes findAll (and returns iterable of matches)
	 *  
	 * @param expression pattern string
	 */
	public RegExp(String expression) {
		super(1,2);
		pattern = Pattern.compile(expression);
	}
	private IFn matcherPrepareFn = null;
	// allows function to set flags on the matcher
	public IFn setMatcherPrepareFn(IFn matcherPrepareFn) {
		IFn ret = this.matcherPrepareFn;
		this.matcherPrepareFn = matcherPrepareFn;
		return ret;
	}
	/**
	 * Returns matches of this regex pattern in the input. 
	 * The returned iterable contains either strings (if regex pattern contains no groups) or 
	 * arrays of Strings, each containing all groups (including group 0 - whole match).
	 * 
	 * In case of CharSequence the Strings in matches are not copies (and thus reference the input)
	 * In case of Readable the Strings in matches are copies and the buffer size is 4 MB
	 * 
	 * With Readable parameter the look-behind length is limited to one quarter of the buffer
	 * and transparent bounds have to be turned on.
	 *  
	 * @param input a CharSequence or Readable
	 * @return Iterable of Strings or arrays of Strings
	 */
	@SuppressWarnings("unchecked")
	public <T> T findAll(Object input) {
		if (input instanceof CharSequence) {
			return (T)findAll((CharSequence)input, false);
		} else {
			return (T)findAllReadable((Readable)input, null);
		}
	}
	/**
	 * Returns matches of this regex pattern in the input. 
	 * The returned iterable contains either strings (if regex pattern contains no groups) or 
	 * arrays of Strings, each containing all groups (including group 0 - whole match). 
	 * 
	 * In case of CharSequence the option is boolean (true if match Strings should be copies)
	 * In case of Readable the option is buffer size. With readables match Strings are always copies.
	 * 
	 * @param input a CharSequence or Readable
	 * @param option a boolean or integer  
	 * @return Iterable of Strings or arrays of Strings
	 */
	@SuppressWarnings("unchecked")
	public <T> T findAll(Object input, Object option) {
		if (input instanceof CharSequence) {
			return (T)findAll((CharSequence)input, ((Boolean)option).booleanValue());
		} else {
			return (T)findAllReadable((Readable)input, (Integer)option);
		}
	}	
		
	@SuppressWarnings("unchecked")
	private <T> T findAll(final CharSequence input, final boolean copyMatches) {
        class FindAllIter extends SingleCheckIterator<Object> {
        	FindAllIter(Matcher m) {
        		this.m = m;
        	}
            Matcher m;
            {
                if (m != null && matcherPrepareFn != null) {
                    matcherPrepareFn.invoke(m);
                }
            }
            @Override
            protected boolean existsNext() {
                return m.find();
            }

            @Override
            protected Object generateNext() {
            	if (copyMatches) {
            		return groupsCopy(m);
            	} else {
            		return groups(m);
            	}
            }

            @Override
            public SingleCheckIterator<Object> iterator() {
                return new FindAllIter(pattern.matcher(input));
            }       
        }
        return (T)new FindAllIter(null);
	}
    @SuppressWarnings("unchecked")
	private <T> T findAllReadable(Readable input, Integer bufLength) {
    	final ReaderCharSequence in =  bufLength == null ? new ReaderCharSequence(input) : new ReaderCharSequence(input, bufLength);
        class FindAllIter extends SingleCheckIterator<Object> {
        	FindAllIter(Matcher m) {
        		this.m = m;
        	}
        	Matcher m;
        	int endOfLastMatch = 0;
        	int startOfLastMatch = 0;
            {
                if (m != null && matcherPrepareFn != null) {
                    matcherPrepareFn.invoke(m);
                }
            }
            @Override
            protected boolean existsNext() {
            	try {
            		boolean ret = m.find(); 
            		endOfLastMatch = m.end();
            		startOfLastMatch = m.start();
            		return ret;
            	} catch (EndOfReaderException e) {
            		m = pattern.matcher(in);
            		m.region(endOfLastMatch, in.length());
            		return m.find();
            	}
            }

            @Override
            protected Object generateNext() {
            	Object ret = groupsCopy(m);
            	// try to compress the buffer
            	int charsPushedBack = in.doPushBack();
        		if (charsPushedBack != 0) {
        			endOfLastMatch -= charsPushedBack;
        			m.region(m.end()-charsPushedBack, in.length());
        		}
                return ret;
            }

            @Override
            public SingleCheckIterator<Object> iterator() {
                return new FindAllIter(pattern.matcher(in));
            }       
        }
        return (T)new FindAllIter(null);
    }
    private String replaceToString(Object fnOrString, CharSequence input) {
		/*
		 * This code will be super messy because I will pull as many calculations, array allocations,
		 * type checks and general "if" logic out of loops, making them once per function call rather than once per
		 * replace thing. These parameters don't change during a single function invocation, however there's a caveat:
		 * - Unlike earlier versions the replace Fn mustn't alternate between returning a string and string array
		 * - Unlike earlier versions the replace Fn mustn't save or pass around the string array of groups it receives, 
		 * as the contents of this array are changing each replace. If the Fn is trying to save matches in some way, it should copy the array.
		 */
		final Matcher m = pattern.matcher(input);
		if (matcherPrepareFn != null) {
			matcherPrepareFn.invoke(m);
		}			
		StringBuffer buf = new StringBuffer(Math.min(input.length(), 4194304));
		StringBuilder replaceAssembly = new StringBuilder(); // reusable string builder to make replace strings with
		if (fnOrString instanceof IFn) {
			IFn replaceFn = (IFn)fnOrString;
			// unrolled first invocation of (find/groups/append replacement) out of the while loop to be able to skip the class check
			// on non-first replaces and to be able allocate a correctly sized array or to avoid allocating list altogether
			if (m.find()) {   
				// first let's check if we need to put groups in the list to pass to the replace Fn or do we just pass the String
				if (m.groupCount() == 0) { 
					// ok from now on just feed strings (whole match) to the fn
					// expect a string back
					while (m.find()) {
						m.appendReplacement(buf, "");
						buf.append((String)replaceFn.invoke(groups(m)));							
					}					
				} else { // we are feeding the replace fn a string array of match's groups
					// reuse the array each replace
					final String[] matchGroups = new String[m.groupCount()+1]; 
					Object repl = replaceFn.invoke(groups(m, matchGroups));  
					if (repl instanceof String) { // the fn is returning a string do a string replace
						m.appendReplacement(buf, "");
						buf.append((String)repl);
						while (m.find()) {
							repl = replaceFn.invoke(groups(m, matchGroups));
							m.appendReplacement(buf, "");
							buf.append((String)repl);							
						}					
					} else {	// if the fn is returning iterables we do a multiple replace
						m.appendReplacement(buf, "");
						buf.append(groupReplace(m, (String[])repl, replaceAssembly));
						while (m.find()) {
							repl = replaceFn.invoke(groups(m));
							m.appendReplacement(buf, "");
							buf.append(groupReplace(m, (String[])replaceFn.invoke(groups(m, matchGroups)), replaceAssembly));
						}					
					}						
				}
			}
			m.appendTail(buf);				
		} else {
		    // we don't have a replace fn, we have a replace string
		    // do a simple replace
			String replacement = (String)fnOrString;
			while (m.find()) {
				m.appendReplacement(buf, "");
				buf.append(replacement);
			}
			m.appendTail(buf);				
		}
		return buf.toString();
    	
    }
	@Override
	public Object doFunc(Object... args) {
		if (args.length == 1) {
			if (args[0] instanceof CharSequence) {
				return findAll((CharSequence)args[0], false);
			} else {
				return findAllReadable((Readable)args[0], null);
			}
		} else if (args.length == 2) { // do a replace
			if (args[0] instanceof CharSequence) {
				return findAll((CharSequence)args[0], (Boolean)args[1]);
			} else {
				return findAllReadable((Readable)args[0], (Integer)args[1]);
			}
		} else {
		    // do a replace on a specific group
			final Matcher m = pattern.matcher((CharSequence)args[2]);
			if (matcherPrepareFn != null) {
				matcherPrepareFn.invoke(m);
			}			
			StringBuffer buf = new StringBuffer(((String)args[2]).length());
			StringBuilder replaceAssembly = new StringBuilder();
			int replaceGroupIndex = ((Number)args[1]).intValue();
			String[] matchGroups = new String[m.groupCount()+1];
			if (args[0] instanceof IFn) { // check if we are replacing with a function or a string
				IFn replaceFn = (IFn)args[0]; 
				while (m.find()) {
				    m.appendReplacement(buf, "");
				    buf.append(groupReplace(m, replaceGroupIndex, (String)replaceFn.invoke(groups(m)), matchGroups, replaceAssembly));
				}
				m.appendTail(buf);				
			} else {
				String replacement = (String)args[0];
				while (m.find()) {
				    m.appendReplacement(buf, "");
				    buf.append(groupReplace(m, replaceGroupIndex, replacement, matchGroups, replaceAssembly));
				}
				m.appendTail(buf);				
			}
			return buf.toString();
		}
	}
	private Object groups(Matcher m, String[] groupsList) { // array parameter is to facilitate array reuse
		if (m.groupCount() == 0) {
			return m.group();
		}
		for (int i = 0;i <= m.groupCount();i++) {
			groupsList[i] = m.group(i);
		}
		return groupsList;
	}
	private Object groupsCopy(Matcher m, String[] groupsList) { // same as groups method but copies the strings
		if (m.groupCount() == 0) {
			return JVM.currentJVM.copyString(m.group());
		}
		for (int i = 0;i <= m.groupCount();i++) {
			groupsList[i] = JVM.currentJVM.copyString(m.group(i));
		}
		return groupsList;		
	}
	private Object groupsCopy(Matcher m) {
		return groupsCopy(m, new String[m.groupCount()+1]);
	}
	private Object groups(Matcher m) {
		return groups(m, new String[m.groupCount()+1]);
	}
	private String groupReplace(Matcher m, int groupIndex, String replacement, String[] replacementList, StringBuilder outputBuffer) {
		replacementList[groupIndex] = replacement;
		return groupReplace(m, replacementList, outputBuffer);
	}
	// replace all groups with the data in the string array
	// if array is null replace nothing
	private String groupReplace(Matcher m, String[] replacements, StringBuilder outputBuffer) {
		if (replacements == null) {
			return m.group(0);
		}
		outputBuffer.setLength(0); // reset the reusable string builder
		String matchStr = m.group(0);
		int matchStartIdx = m.start(0);
		// extract all group data
		int numberOfGroups = m.groupCount() + 1;
		GroupIndexes[] groupData = new GroupIndexes[numberOfGroups]; 
		for (int i=0;i < numberOfGroups;i++) {
			groupData[i] = new GroupIndexes();
			groupData[i].startIdx = m.start(i);
			groupData[i].endIdx = m.end(i);
			groupData[i].replacement = replacements[i];
		}
		sortGroupIndexes(groupData);
		// marks how far in the original string are we
		int marker = 0;
		for (int i = 0; i < numberOfGroups;i++) {
			// check if group matched and if group wasn't overriden by a replace of a larger group
			if ((groupData[i].startIdx != -1) && !isOverriden(groupData, i)) {
				// add text before the match
				outputBuffer.append(matchStr.substring(marker, groupData[i].startIdx - matchStartIdx));
				if (groupData[i].replacement != null) {  // do we have a replacement
					outputBuffer.append(groupData[i].replacement); // add the replacement
					marker = groupData[i].endIdx - matchStartIdx; // set the marker at the end of original match
				} else {
					marker = groupData[i].startIdx - matchStartIdx; // just correct the marker
				}
			}
		}
		outputBuffer.append(matchStr.substring(marker));
		// Return substring rather than toString, since substring doesn't allocate a new char array
		// This means that any changes to the string builder affect returned string, but that is ok since
		// the substring is absorbed by another StringBuilder right away
		return outputBuffer.substring(0, outputBuffer.length());  
	}
	private boolean isOverriden(GroupIndexes[] groupData, int thisIdx) {
		for (int i = thisIdx - 1;i >= 0;i--) {
			// lets check boundaries of the preceding groups to determine if they contain this group
			// if the replacement of any parent group is not null then this groups' replacement is overriden
			if (groupData[i].replacement != null && groupData[thisIdx].startIdx < groupData[i].endIdx) {
					return true;
			}
		}
		return false;
	}
	
	private static class GroupIndexes implements Comparable<GroupIndexes> {
		public int startIdx;
		public int endIdx;
		public String replacement;

		public int compareTo(GroupIndexes o) {
			return startIdx - o.startIdx;
		}
		
	}
	private void sortGroupIndexes(GroupIndexes[] data) {
		int j = 0;
		GroupIndexes current = null;
		for(int i = 1;i < data.length;i++) {
			int value = data[i].startIdx;
			current = data[i];
			j = i - 1;
			while (j >= 0 && data[j].startIdx > value) {
				data[j + 1] = data[j];
				j--;
			}
			data[j + 1] = current;
		}
	}
	
	/**
	 * Replaces matches of this regex in the input string with the replacement(s). 
	 * 
	 * Replacement parameter can either be:
	 * <ul>
	 * <li> A String (will replace all the matches)<li>
	 * <li> A function of one argument.</li>
	 * </ul>
	 * 
	 * The replace function receives the match as an argument. 
	 * The argument is a String if regex pattern contains no groups, 
	 * otherwise the argument is an array of Strings, each String belonging to a group.
	 * Note that group 0 is whole match and the first group in the regex is under index 1.
	 * 
	 * The replace function should return either:
	 * <ul>
	 * <li> A String (will replace whole match) - mandatory if no groups were used. Mustn't be null.</li>
	 * <li> An array of Strings, one for each group including group 0. 
	 * This can be the same array the as the functions's parameter containing
	 * the match's groups (i.e. you can get performance improvement from reusing an already allocated array).
	 * Each group will then be replaced by the appropriate string. 
	 * If a group isn't supposed to be replaced, then its position in the array of replacements
	 * should be occupied by a null value. If groups in the pattern are nested, the outer group's
	 * replacement (if any) takes the precedence. If the returned string array is 
	 * null replace skips the whole replacing process for this match.
	 * </li>
	 * </ul>
	 *
	 * 
	 * <b>Characters \ and $ have special meaning in the replacement string parameter per java regex rules.
	 * This method however ignores this rule and these characters don't need to be escaped in replacement strings.
	 * <b>
	 *  
	 * @param input A String input
	 * @param replacement A String or a Function
	 * 
	 * @return
	 */
	public String replace(Object replacement, Object input) {
		return (String)this.invoke(replacement, input);
	}

	/**
	 * Replaces matches of this regex in the input string with the replacement(s). 
	 * 
	 * Replacement parameter can either be:
	 * <ul>
	 * <li> A String (will replace all the matches)<li>
	 * <li> A function of one argument.</li>
	 * </ul>
	 * 
	 * The replace function receives the match as an argument. 
	 * The argument is a String if regex pattern contains no groups, 
	 * otherwise the argument is an array of Strings, each String belonging to a group.
	 * 
	 * The result of the function or the string parameter will replace n-th group.
	 *    
     * <b>Characters \ and $ have special meaning in the replacement string parameter per java regex rules.
     * This method however ignores this rule and these characters don't need to be escaped in replacement strings.
     * <b>
	 * 
	 * @param replacement A String or a function
	 * @param groupNum A number - index of the group to replace (0-based -> group 0 is index 0)
	 * @param input String input
	 * @return
	 */
	public String replace(Object replacement, Object groupNum, Object input) {
		return (String)this.invoke(replacement, groupNum, input);
	}

	/**
	 * Returns a new RegExp instance. This is short-hand for "new RegExp(o)"
	 * @param o a String
	 * @return new RegExp instance
	 */
	public static RegExp $R(Object o) {
		return new RegExp((String)o);
	}
	
	// helper premade function classes to make implementation of replace function easier
	public static abstract class OneGroupOneReplaceFn extends Fn {
		public OneGroupOneReplaceFn() {
			super(1,1);
		}
		@Override
		public String doFunc(Object... args) {
			return getReplacementString((String)args[0]);
		}
		public abstract String getReplacementString(String match); 
	}
	public static abstract class MulGroupOneReplaceFn extends Fn {
		public MulGroupOneReplaceFn() {
			super(1,1);
		}
		@Override
		public String doFunc(Object... args) {
			return getReplacementString((String[])args[0]);
		}
		public abstract String getReplacementString(String[] matchGroups); 
	}
	// note that matchgroups has one more elements than returned iterable
	public static abstract class MulGroupMulReplaceFn extends Fn {
		public MulGroupMulReplaceFn() {
			super(1,1);
		}
		@Override
		public String[] doFunc(Object... args) {
			return getReplacementSeq((String[])args[0]);
		}
		public abstract String[] getReplacementSeq(String[] matchGroups); 
	}
	public static final Fn split = new SplitFn();
	/**
	 * Splits the input String using a RegExp instance into an Iterable of Strings.
	 * Returns null if input String is null
	 * @author rok.lenarcic
	 *
	 */
	@FnName("Regular Exp. split")
	private static class SplitFn extends Fn {
		public SplitFn() {
			super(4,5);
		}
		private static List<String> ONE_STRING_LIST = Collections.singletonList(""); 
		@Override
		public Iterable<String> doFunc(Object... args) {
			final boolean charSeq = args[3] instanceof CharSequence;
			
			final CharSequence input = charSeq ? (CharSequence)args[3] : null;
			final ReaderCharSequence inputReadable = charSeq ? null :
				args.length == 4 ? new ReaderCharSequence((Readable)args[3]) : new ReaderCharSequence((Readable)args[3], (Integer)args[4]);
						
			final RegExp exp = ((RegExp)args[0]);
			final boolean keepEmptyStr = (Boolean)args[2];
			final boolean includeMatches = (Boolean)args[1];
			class ResIter extends SingleCheckIterator<String> {
			    private int lastBoundary = 0;
			    private Matcher m = exp.pattern.matcher(input);
			    private String pieceToReturn = null;
			    private String matchToken = null;
			    {
			         if (exp.matcherPrepareFn != null) {
			                exp.matcherPrepareFn.invoke(m);
			         }
			    }
	            @Override
	            public SingleCheckIterator<String> iterator() {
	                return new ResIter();
	            }
	            @Override
	            protected boolean existsNext() {
	            	String tok;
	            	while((tok = nextToken()) != null) {
	            		if (!tok.equals("") || keepEmptyStr) {
	            			pieceToReturn = tok;
	            			return true;
	            		}
	            	}
	            	return false;
	            }
	            private String nextToken() {
	            	if (matchToken != null) {
	            		String tmp = matchToken;
	            		matchToken = null;
	            		return tmp;
	            	}
	            	if (m.hitEnd()) {
	            		return null;
	            	}
    				String ret = null;
	    			while (ret == null && m.find()) {
	    				if (m.start() != 0) {
		    				if (includeMatches) {
		    					if (m.start() == m.end()) {
		    						matchToken = ""; // faster than substring
		    					} else {

		    						matchToken = JVM.currentJVM.copyString(input.subSequence(m.start(),m.end()).toString());
		    					}
		    				}
	    					if (lastBoundary == m.start()) {
	    						ret = "";
	    					} else {
	    						ret = JVM.currentJVM.copyString(input.subSequence(lastBoundary, m.start()).toString());
	    					}
	    				} else { // if we matched at start then we have no leading string
		    				if (includeMatches) {
		    					if (m.start() == m.end()) {
		    						ret = ""; // faster than substring
		    					} else {
		    						ret = JVM.currentJVM.copyString(input.subSequence(m.start(),m.end()).toString());
		    					}
		    				}
	    				}
	    				lastBoundary = m.end();
	    			}
	    			if (ret == null && lastBoundary != input.length()) {
	    				ret = JVM.currentJVM.copyString(input.subSequence(lastBoundary,input.length()).toString());
	    				lastBoundary = input.length();
	    			}
	    			return ret;
	            }
	            @Override
	            protected String generateNext() {
	               return pieceToReturn;
	            }
			}
			if (input == null) {
				return null;
			} else if ("".equals(input) && keepEmptyStr) {
				return ONE_STRING_LIST;
			} else {
				return new ResIter();
			}
		}

	}
	/**
	 * Splits the input CharSequence or Readable using this regular expression.
	 * If Readable is used lookbehind, $ and ^ expressions may work incorrectly (unable to cross buffer boundaries backward). 
	 * If include matches flag is true then the matches themselves are returned too (unlike String.split).
	 * If matches are not included it acts the same as String.split except that this function won't return an
	 * empty string if the split expression matches the input string at the beginning e.g.:
	 * 
	 * Regex: "a" Input: "ab"
	 * String.split -> "", "b"
	 * This fn with includeMatches false: "b"
	 *   
	 * @param includeMatches boolean
	 * @param keepEmptyStr boolean
	 * @param input a CharSequence or Readable
	 * @return 
	 */
	@SuppressWarnings("unchecked")
	public Iterable<String> split(Object includeMatches, Object keepEmptyStr, Object input) {
		return (Iterable<String>)split.invoke(this,includeMatches, keepEmptyStr, input);
	}
	@SuppressWarnings("unchecked")
	public Iterable<String> split(Object includeMatches, Object keepEmptyStr, Object input, int bufferLength) {
		return (Iterable<String>)split.invoke(this,includeMatches, keepEmptyStr, input, bufferLength);
	}	
	public static class ReaderCharSequence implements CharSequence {
	    private static final int DEFAULT_BUFFER_LENGTH = 4194304; // 4 MB
	    private CharBuffer buf;
	    private Readable readable;
	    private int apparentSequenceLength = Integer.MAX_VALUE;
	    public ReaderCharSequence(Readable reader) {
	        this(reader, DEFAULT_BUFFER_LENGTH);
	    }
        private boolean ensureData(int idx) {
        	CharBuffer buf = ensureCapacity(idx);
    		if (idx >= buf.limit()) {
	    		buf.position(buf.limit());
	    		buf.limit(buf.capacity());
	        	try {
					while (idx >= buf.position()) {
						int charsRead = readable.read(buf);
						if (charsRead == -1) {
					    	apparentSequenceLength = buf.position();
					    	buf.limit(buf.position());
					    	buf.rewind();
					    	return false;      			
						} 
					}
					buf.limit(buf.position());
					buf.rewind();
				} catch (IOException e) {
					throw new FnException(e);
				}
    		}
    		return true;
        }

        // potentially returns a new larger buffer with position non-rewound
        private CharBuffer ensureCapacity(int idx) {
        	while (idx >= buf.capacity()) {
        		CharBuffer origBuff = buf;
                CharBuffer newBuf = CharBuffer.allocate(origBuff.capacity()*2);
                newBuf.put(origBuff);
                newBuf.limit(origBuff.limit());
                buf = newBuf;
        	}
        	return buf;
        }
        public ReaderCharSequence(Readable reader, int bufferLength) {
	        this.readable = reader;
	        this.buf = CharBuffer.allocate(bufferLength);
	        this.buf.limit(0);
	    }
		public CharSequence subSequence(int start, int end) {
			lastSubseqStart = start;
			return buf.subSequence(start, end);				
		}

        public char charAt(int arg0) {
        	if (ensureData(arg0)) {
                return buf.get(arg0);        		
        	} else {
        		throw new EndOfReaderException();
        	}
        }
        public int length() {
            return apparentSequenceLength;
        }
        private int lastSubseqStart = 0;
        // returns how much the buffer was compacted after last subsequence
        public int doPushBack() {
        	int numOfCharsPushedBack = 0;
			if (lastSubseqStart > ((((long)buf.capacity())*7) / 10)) {  
				// after the match start goes over 70% it's time to compact
				buf.position(Math.min(buf.capacity() / 2, buf.limit()));
				numOfCharsPushedBack = buf.position();
				buf.compact();
				buf.limit(buf.position());
				buf.position(0);
			} 
        	return numOfCharsPushedBack;
        }

        public static class EndOfReaderException extends RuntimeException {

			/**
			 * 
			 */
			private static final long serialVersionUID = 1L;
        	
        }
	}
}
